/*
 *  $Id: MutablePropBean.java,v 1.1 2008/03/24 11:19:49 shingoki Exp $
 *
 *  Copyright (c) 2008 shingoki
 *
 *  This file is part of jpropeller, see http://jpropeller.sourceforge.net
 *
 *    jpropeller is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    jpropeller is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with jpropeller; if not, write to the Free Software
 *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
package org.jpropelleralt.map.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.BoxFeatures;
import org.jpropelleralt.box.impl.BoxFeaturesDefault;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.map.MapBox;
import org.jpropelleralt.map.MapDelta;
import org.jpropelleralt.multiverse.Multiverse;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.universe.Universe;
import org.jpropelleralt.utils.impl.ContentsTracking;

/**
 * An {@link MapBoxDefault} wraps an underlying {@link Map} implementation, 
 * delegating actual storage of elements to this core map, and adding 
 * tracking of the elements so that events are generated as required 
 * for {@link MapBox} compliance.
 * 
 * @param <K> 		The type of key in the map.
 * @param <V>		The type of element in the map.
 */
public class MapBoxDefault<K, V> implements MapBox<K, V> {

	//Reference counter for elements in map
	private final ContentsTracking tracking;

	private final BoxFeatures features;
	private final MapBoxReaction reaction;
	
	//The Map we delegate to for actual storage, etc.
	private final Map<K, V> core;
	
	/**
	 * Create a new {@link MapBoxDefault} based on a given core
	 * map. The core {@link Map} provides the actual storage and
	 * implementation of {@link Map} methods - this shell just
	 * intercepts method calls to keep track of the {@link Map}
	 * contents to implement {@link MapBox}, for example 
	 * by firing the proper events on element changes, etc.
	 * 
	 * NOTE: you must NOT modify the core {@link Map} after using it
	 * to create an {@link MapBoxDefault}, otherwise you will stop the
	 * {@link MapBoxDefault} functioning as a compliant {@link MapBox}.
	 * It is safest not to retain a reference to the core {@link Map} at all,
	 * e.g.
	 * <code>
	 * 		MapBox<String, String> map = 
	 * 			new MapBoxDefault(new TreeMap<String, String>());
	 * </code>
	 * 
	 * @param core	This {@link MapBoxDefault} will delegate to the specified 
	 * 				{@link Map}. If this is null, a new {@link HashMap} will
	 * 				be used.
	 * @param universe	The {@link Universe} this will belong to
	 */
	public MapBoxDefault(Map<K, V> core, Universe universe) {
		super();
		
		//If core is null, use an empty HashMap
		if (core == null) core = new HashMap<K, V>();
		
		this.core = core;
		this.features = new BoxFeaturesDefault(this, universe);
		this.reaction = new MapBoxReaction();
		
		//This will help ensure we are always listening to our own contents
		tracking = new ContentsTracking(reaction);
		
		//Set up initial tracking of contents of provided map
		retrackAll();
	}
	
	@Override
	public BoxFeatures features() {
		return features;
	}

	/**
	 * Start tracking all elements in the list again. 
	 * clearAllTracking() is called, then all elements
	 * are passed to startTrackingElement(e);
	 */
	private void retrackAll() {
		//Stop tracking all elements
		tracking.clearAllTracking();
		
		//Track each value in the map, making sure that we start tracking
		//it once for EACH MAPPING that points to that value
		//Hence we don't iterate the value set, which contains values
		//mapped-to by multiple keys only once - we iterate the key set,
		//then follow each key to the value.
		//This is so that we won't stop tracking a value until all mappings
		//to it are removed.
		for (K k : core.keySet()) {
			tracking.startTrackingElement(core.get(k));
		}
	}
	
	/**
	 * Perform a change to the core map in a way that preserves proper tracking,
	 * and then fires an event for a complete change to the map
	 * @param action
	 * 		The action to perform to the map - may manipulate the core map in
	 * any way, but should not do anything else.
	 * 		The action must NOT throw a non-runtime exception - if it does, an
	 * {@link CCollectionRuntimeException} will be thrown with the 
	 * exception as its cause.
	 * Map methods do not throw non-runtime exceptions, so the action should not
	 * either. 
	 * 		Runtime exceptions from the action will just be thrown from this method,
	 * but only after tracking has been set up, and a prop event fired for a 
	 * complete list change
	 * @param oldSize
	 * 		The size of the map before the change (that is, at the time that 
	 * this method is called)
	 * @return
	 * 		The return value of the action
	 * @throws CCollectionRuntimeException
	 * 		If the action throws a non-runtime exception
	 */
	private boolean trackAroundMapChange(Callable<Boolean> action) {
		
		int oldSize = -1;
		Multiverse.prepareWrite(this);
		try {

			 oldSize = size();

			//To start with, clear all references, and stop listening to all contents.
			//The action is assumed to be a major enough operation that we don't 
			//try to track it in detail
			tracking.clearAllTracking();
			
			try {
				return action.call();
			} catch (Exception e) {
				//We do not expect a non-runtime exception from the action, this is an error
				throw new RuntimeException("Unexpected exception from a map action", e);
			}
			
		//Always reinstate tracking and conclude change - the action may complete, or it
		//may throw an exception after adding some elements 
		} finally {
			
			//Reinstate tracking on all elements
			retrackAll();
			
			//Start a list change
			//showing we changed everything
			MapDelta mapChange = MapDeltaDefault.newCompleteChange(size(), oldSize);
			Multiverse.propagateWrite(this, MapChangeDefault.shallow(mapChange));
			
			Multiverse.concludeWrite(this);
		}
		
	}

	
	//#####################################################################
	//
	//	The following methods modify the set, and require tracking of elements,
	//	etc.
	//
	//#####################################################################
	
	public void clear() {
		
		//Make a callable that will do the clear
		Callable<Boolean> action = new Callable<Boolean>() {
			@Override
			public Boolean call() throws Exception {
				core.clear();
				return true;
			}
		};

		//TODO we should throw a CLEAR instead of a complete change 
		//(as thrown by trackAroundMapChange) after this, but it 
		//doesn't make too much difference
		
		//Perform the action with tracking
		trackAroundMapChange(action);
		
	}
	
	public V put(K key, V value) {
	
		Multiverse.prepareWrite(this);
		
		try {
			//Do we have an existing value? This affects the "old size" of the
			//map change we will fire
			boolean existingValue = core.containsKey(key);
			
			//Try to put in core - if we get a runtime exception the 
			//value is not put, so nothing to do
			V oldValue = core.put(key, value);
			
			//Note that null values are ignored for tracking, so no need
			//to check whether mapping was actually present
			tracking.stopTrackingElement(oldValue);
	
			tracking.startTrackingElement(value);
	
			//Start a map change
			//showing the put
			MapDelta change;
			if (existingValue) {
				change = MapDeltaDefault.newSingleKeyAlteration(this, key);
			} else {
				change = MapDeltaDefault.newInsertionChange(this, key);			
			}
			Multiverse.propagateWrite(this, MapChangeDefault.shallow(change));
	
			return oldValue;
			
		//Make sure we always conclude the change
		} finally {
			Multiverse.concludeWrite(this);
		}
	}
	
	public void putAll(final Map<? extends K, ? extends V> m) {

		//Make a callable that will do the addAll
		Callable<Boolean> action = new Callable<Boolean>() {
			@Override
			public Boolean call() throws Exception {
				core.putAll(m);
				return true;
			}
		};
		
		//Perform the action with tracking
		trackAroundMapChange(action);
	}
	
	@Override
	public void replace(final Map<? extends K, ? extends V> newContents) {
		//Make a callable that will do the clear and addAll
		Callable<Boolean> action = new Callable<Boolean>() {
			@Override
			public Boolean call() throws Exception {
				core.clear();
				core.putAll(newContents);
				return true;
			}
		};
		
		//Perform the action with tracking
		trackAroundMapChange(action);
	}

	@Override
	public void reactionReplace(Map<? extends K, ? extends V> newContents, boolean propagate) {
		if (propagate) {
			replace(newContents);
		} else {
			core.clear();
			core.putAll(newContents);
		}
	}
	
	//Cast to K is safe, since we check that the key is in the map
	//before casting, and if it is in the map it is of type K
	@SuppressWarnings("unchecked")
	public V remove(Object key) {
		
		Multiverse.prepareWrite(this);
		try {
			
			//If the key is not present, then nothing to do
			if (!core.containsKey(key)) {
				return null;
			}

			//The key IS present, so is of type K
			K k = (K)key;
			
			//Try to remove from core - if we get a runtime exception the 
			//value is not removed, so nothing to do
			V oldValue = core.remove(k);
			
			//Note that null values are ignored for tracking, so no need
			//to check whether mapping was actually present
			tracking.stopTrackingElement(oldValue);
	
			//Start a map change
			//showing the removal
			MapDelta change = MapDeltaDefault.newRemoveChange(this, k);
			Multiverse.propagateWrite(this, MapChangeDefault.shallow(change));
			
			return oldValue;
	
		//Always conclude change
		} finally {
			Multiverse.concludeWrite(this);
		}
	}

	//#####################################################################
	//
	//	The following methods require wrappers to prevent
	//	modifications that would not currently be noticed by the wrapper,
	//	and to enforce locking.
	//
	//#####################################################################

	public Collection<V> values() {
		Multiverse.prepareRead(this);
		try {		
			return new CollectionShell<V>(core.values());
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public Set<Entry<K, V>> entrySet() {
		Multiverse.prepareRead(this);
		try {
			return new SetShell<Entry<K,V>>(core.entrySet());
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public Set<K> keySet() {
		Multiverse.prepareRead(this);
		try {
			return new SetShell<K>(core.keySet());
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	
	/**
	 *	Wraps an iterator, and makes main class fire property change (complete list
	 *	change) when remove is used, and adds appropriate locking.
	 */
	private class IteratorShell<T> implements Iterator<T>{
		
		//The wrapped iterator
		private final Iterator<T> it;
		
		/**
		 * Make a wrapper 
		 * @param it The iterator to wrap
		 */
		private IteratorShell(Iterator<T> it) {
			this.it = it;
		}

		//Method must ensure listbean compliance
		public void remove() {
//TODO this could work, using code similar to that below, if the wrapped
//iterator supports remove.
//			//Make a callable that will do the remove
//			Callable<Boolean> action = new Callable<Boolean>() {
//				@Override
//				public Boolean call() throws Exception {
//					it.remove();
//					return true;
//				}
//			};
//			
//			//Perform the action with tracking
//			trackAroundListChange(action);
			throw new UnsupportedOperationException("Cannot remove from CMapDefault-derived iterators");
		}
		
		//Methods delegated directly to the wrapped iterator
		public boolean equals(Object obj) {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return it.equals(obj);
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public int hashCode() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return it.hashCode();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public boolean hasNext() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return it.hasNext();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public T next() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return it.next();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public String toString() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return it.toString();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
	}

	/**
	 * Wrap a collection to make it unmodifiable, and to respect locking, 
	 * and to pass on these restrictions to iterators on it
	 */
	private class CollectionShell<T> implements Collection<T> {
		private final Collection<T> coreCollection;

		private CollectionShell(Collection<T> coreList) {
			this.coreCollection = Collections.unmodifiableCollection(coreList);
		}

		private CollectionShell(Collection<T> coreList, boolean alreadyUnmodifiable) {
			this.coreCollection = alreadyUnmodifiable ? coreList : Collections.unmodifiableCollection(coreList);
		}

		//This is complex - must wrap the core iterator to pass on
		//locking
		public Iterator<T> iterator() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return new IteratorShell<T>(coreCollection.iterator());
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		
		//These methods are simple - just lock around reading
		public boolean contains(Object o) {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.contains(o);
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public boolean containsAll(Collection<?> c) {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.containsAll(c);
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public boolean equals(Object o) {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.equals(o);
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public int hashCode() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.hashCode();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public boolean isEmpty() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.isEmpty();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public int size() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.size();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public Object[] toArray() {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.toArray();
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}
		public <S> S[] toArray(S[] a) {
			Multiverse.prepareRead(MapBoxDefault.this);
			try {
				return coreCollection.toArray(a);
			} finally {
				Multiverse.concludeRead(MapBoxDefault.this);
			}
		}	
		
		//Methods that modify the collection will all fail due to unmodifiable view
		//TODO: Note we could actually support these without a huge effort by tracking around
		//changes
		public boolean remove(Object o) {
			return coreCollection.remove(o);
		}
		public boolean removeAll(Collection<?> c) {
			return coreCollection.removeAll(c);
		}
		public boolean retainAll(Collection<?> c) {
			return coreCollection.retainAll(c);
		}
		public boolean add(T e) {
			return coreCollection.add(e);
		}
		public boolean addAll(Collection<? extends T> c) {
			return coreCollection.addAll(c);
		}
		public void clear() {
			coreCollection.clear();
		}
		
	}
	
	/**
	 * Wrap a Set to make it unmodifiable, and to respect locking, 
	 * and to pass on these restrictions to iterators on it
	 * Note: This is pretty much identical to CollectionShell, since
	 * Set is the same as Collection, apart from contracts.
	 */
	private class SetShell<T> extends CollectionShell<T> implements Set<T>{
		private SetShell(Set<T> coreSet) {
			super(Collections.unmodifiableSet(coreSet), 
					true);	//already unmodifiable - super won't wrap again
		}
	}

	
	//#####################################################################
	//
	//	The following methods are passed to the core map, but may read
	//	Changeable state so are read-locked.
	//
	//#####################################################################
	
	public boolean equals(Object o) {
		Multiverse.prepareRead(this);
		try {
			return core.equals(o);
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public V get(Object key) {
		Multiverse.prepareRead(this);
		try {
			return core.get(key);
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public int hashCode() {
		Multiverse.prepareRead(this);
		try {
			return core.hashCode();
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public boolean isEmpty() {
		Multiverse.prepareRead(this);
		try {
			return core.isEmpty();
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public int size() {
		Multiverse.prepareRead(this);
		try {
			return core.size();
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public boolean containsKey(Object key) { 
		//	since they have no interactions with the events/b
		Multiverse.prepareRead(this);
		try {
			return core.containsKey(key);
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	public boolean containsValue(Object value) {
		Multiverse.prepareRead(this);
		try {
			return core.containsValue(value);
		} finally {
			Multiverse.concludeRead(this);
		}
	}
	

	/**
	 * A {@link Reaction} that just makes sure the {@link MapBox}
	 * changes when its contents do.
	 */
	private class MapBoxReaction implements Reaction {

		Map<Box, Change> defaultChanges = new IdentityHashMap<Box, Change>();
		
		private MapBoxReaction() {
			//When any contained Box changes, we have a deep change,
			//where the entire map alters.
			//TODO would be nice to restrict the delta, to just give the key(s)
			//of the changed Box.
			defaultChanges.put(
					MapBoxDefault.this, 
					MapChangeDefault.deep(MapBoxDefault.this));
		}
		
		@Override
		public void apply(boolean propagate) {
			//Nothing to do when we apply - we just propagate changes
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			//We only listen to contents of the map, so
			//if we see any change, it indicates a deep change to
			//map
			return defaultChanges;
		}
		
		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			//Nothing to do
			return false;
		}
	}

}
